#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""General purpose form-based authentication component for CherryPy.
Its behaviour is based on Turbogears' Identity.
If it isn't exactly what you need, try subclassing it. It has been written
with this purpose in mind.

Requires the Session tool to be loaded:
http://www.cherrypy.org/wiki/CherryPySessions
"""
import cherrypy
from cherrypy import config, HTTPRedirect, request, response
from webpyte import flash, has_flash
from webpyte.i18n import gettext as _


class AuthBase(object):
    """Base class for authentication. Subclasses must:
    
    1) override this method (make sure you read its docstring):
       def get_user(self, login)
    
    2) Expose the login_page() and authenticate() methods, according to your
    request dispatcher.
    """
    
    def get_user(self, login):
        """This method must be overridden in subclasses to return a User
        object from their unique login. The returned object should have
        these properties:
        - login: gets a string containing the unique login.
        - password: gets this user's password, possibly encrypted.
        - groups: gets a set containing the user's groups.
        - permissions: gets a set containing the user's permissions.
        - display_name: gets the user name for showing in the page.
        """
        raise NotImplementedError()
    
    def check_credentials(self, login, password):
        """Verifies that the provided credentials are valid. Returns the
        user object, or a string describing the authentication problem,
        or simply None.
        """
        try:
            u = self.get_user(login)
        except Exception, e:
            return unicode(e)
        if u is None:  return _("Unknown login: ") + login
        if isinstance(u, basestring):  return u
        if u.password != self.encrypt(password):
            return _("Incorrect password.")
        return u
    
    def encrypt(self, password):
        """This version does not really encrypt the password;
        instead just returns it. But a subclass could do, for example:
        
            return md5.new(password).hexdigest()
        """
        return password
    
    session_key = 'tb_login'
    login_url = "/login"
    
    def login_page(self):
        """Controller for the login page. Subclasses must expose this."""
        if has_flash():
            response.status = 403 # Forbidden
        return dict(
            action = "authenticate", # exposed method
        )
    
    def authenticate(self, user_name, password, login=None):
        """Receives data from the login form. Subclasses must expose this."""
        # Ignore *login*, it is the stupid submit button.
        o = self.check_credentials(user_name, password)
        if not o or isinstance(o, basestring):
            self.authentication_fail(o)
        else:
            self.authenticate_directly(o)
            wanted_url = cherrypy.session.pop("wanted_url", "/")
            raise HTTPRedirect(wanted_url, status=303)
    
    def authenticate_directly(self, user):
        request.user = user
        cherrypy.session[self.session_key] = user.login
    
    def authentication_fail(self, msg=None):
        if msg is None:
            msg = _("The credentials you supplied were not correct.")
        flash(msg)
        raise HTTPRedirect(self.login_url, status=303)
    
    def authorization_fail(self, msg=None):
        cherrypy.session["wanted_url"] = request.path_info
        if self.current_login:
            if msg is None:
                msg = _("You are authenticated, but your credentials did not grant access to this resource.")
            flash(msg)
        raise HTTPRedirect(self.login_url, status=303)
    
    def current_login():
        doc = "Gets from the session the login of the logged user, or None."
        def fget(self):
            return cherrypy.session.get(self.session_key, default=None)
        return locals()
    current_login = property(**current_login())
    
    def current_user():
        doc = """Gets the object that represents the logged user, or None.
        If necessary, looks up the database. Memoized.
        """
        def fget(self):
            if hasattr(request, "user"):   # If it is already cached,
                return request.user        # just return it.
            else:
                login = self.current_login # Otherwise get login from session,
                if login is None:
                    return None
                else:
                    request.user = self.get_user(login) # look up database,
                    return request.user       # and return the User object.
        return locals()
    current_user = property(**current_user())
    
    def logout(self):
        if hasattr(request, "user"):  del request.user
        if cherrypy.session.get(self.session_key, None):
            del cherrypy.session[self.session_key]
        raise HTTPRedirect("/", status=303)
    
